package com.fuyao.cloud.admin.flow.service.flowable.impl;

import com.alibaba.nacos.common.utils.StringUtils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fuyao.cloud.admin.flow.converter.CustomBpmnJsonConverter;
import com.fuyao.cloud.admin.flow.dto.ModelInfoVo;
import com.fuyao.cloud.admin.flow.enm.ModelFormStatusEnum;
import com.fuyao.cloud.admin.flow.model.ModelInfo;
import com.fuyao.cloud.admin.flow.model.User;
import com.fuyao.cloud.admin.flow.service.flowable.IFlowableBpmnService;
import com.fuyao.cloud.admin.flow.service.flowable.IFlowableModelService;
import com.fuyao.cloud.admin.flow.service.flowable.IModelInfoService;
import com.fuyao.cloud.common.core.constant.CommonConstants;
import com.fuyao.cloud.common.core.util.R;
import com.fuyao.cloud.common.security.service.CloudUser;
import com.fuyao.cloud.common.security.util.SecurityUtils;
import org.flowable.bpmn.converter.BpmnXMLConverter;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.editor.language.json.converter.BaseBpmnJsonConverter;
import org.flowable.editor.language.json.converter.util.CollectionUtils;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.repository.Deployment;
import org.flowable.ui.common.util.XmlUtil;
import org.flowable.ui.modeler.domain.AbstractModel;
import org.flowable.ui.modeler.domain.Model;
import org.flowable.ui.modeler.model.ModelRepresentation;
import org.flowable.ui.modeler.service.ConverterContext;
import org.flowable.ui.modeler.serviceapi.ModelService;
import org.flowable.validation.ProcessValidator;
import org.flowable.validation.ProcessValidatorFactory;
import org.flowable.validation.ValidationError;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.List;


@Service
public class FlowableBpmnServiceImpl implements IFlowableBpmnService {

    private static final String BPMN_EXTENSION = ".bpmn";
    private static final String BPMN20_XML_EXTENSION = ".bpmn20.xml";

    @Autowired
    private IModelInfoService modelInfoService;
    @Autowired
    private ModelService modelService;
    @Autowired
    private RepositoryService repositoryService;
    @Autowired
    private IFlowableModelService flowableModelService;
    @Autowired
    protected CustomBpmnJsonConverter bpmnJsonConverter;
    @Autowired
    protected BpmnXMLConverter bpmnXMLConverter;
    @Autowired
    private ProcessValidatorFactory processValidatorFactory;

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public R<String> validateBpmnModel(String modelId, String fileName, InputStream modelStream) {
        R<String> returnVo;
        Model processModel = modelService.getModel(modelId);
        if (StringUtils.isBlank(fileName)) {
            fileName = processModel.getKey() + BPMN_EXTENSION;
        }
        try {
            XMLInputFactory xif = XmlUtil.createSafeXmlInputFactory();
            InputStreamReader xmlIn = new InputStreamReader(modelStream, StandardCharsets.UTF_8);
            XMLStreamReader xtr = xif.createXMLStreamReader(xmlIn);
            BpmnModel bpmnModel = bpmnXMLConverter.convertToBpmnModel(xtr);
            bpmnModel.getMainProcess().setId(processModel.getKey());
            bpmnModel.setTargetNamespace(BaseBpmnJsonConverter.NAMESPACE);
            if (CollectionUtils.isEmpty(bpmnModel.getProcesses())) {
                returnVo = R.failed("No process found in definition " + fileName);
                return returnVo;
            }
            if (bpmnModel.getLocationMap().size() == 0) {
                returnVo = R.failed("No required BPMN DI information found in definition " + fileName);
                return returnVo;
            }
            returnVo = this.validationErrors(bpmnModel);
        } catch (XMLStreamException e) {
            returnVo = R.failed( "bpmn.js failed for " + fileName + ", error message " + e.getMessage());
            return returnVo;
        }
        return returnVo;
    }

    private R<String> validationErrors(BpmnModel bpmnModel) {
        R<String> returnVo = R.ok();
        ProcessValidator processValidator = processValidatorFactory.createDefaultProcessValidator();
        List<ValidationError> validationErrors = processValidator.validate(bpmnModel);
        if (CollectionUtils.isNotEmpty(validationErrors)) {
            StringBuffer message = new StringBuffer();
            validationErrors.forEach(validationError -> message.append(validationError.toString()).append("\n"));
            returnVo = R.failed( message.toString());
            returnVo.setCode(-1);
            return returnVo;
        }
        return returnVo;
    }

    @Override
    public R<String> importBpmnModel(String modelId, String fileName, InputStream modelStream, CloudUser user) {
        Model processModel = modelService.getModel(modelId);
        if (StringUtils.isBlank(fileName)) {
            fileName = processModel.getKey() + BPMN_EXTENSION;
        }
        if ((fileName.endsWith(BPMN_EXTENSION) || fileName.endsWith(BPMN20_XML_EXTENSION))) {
            try {
                XMLInputFactory xif = XmlUtil.createSafeXmlInputFactory();
                InputStreamReader xmlIn = new InputStreamReader(modelStream, StandardCharsets.UTF_8);
                XMLStreamReader xtr = xif.createXMLStreamReader(xmlIn);
                BpmnModel bpmnModel = bpmnXMLConverter.convertToBpmnModel(xtr);
                bpmnModel.getMainProcess().setId(processModel.getKey());
                bpmnModel.setTargetNamespace(BaseBpmnJsonConverter.NAMESPACE);
                if (CollectionUtils.isEmpty(bpmnModel.getProcesses())) {
                    return R.failed( "No process found in definition " + fileName);
                }
                if (bpmnModel.getLocationMap().size() == 0) {
                    return R.failed( "No required BPMN DI information found in definition " + fileName);
                }
                ConverterContext converterContext = new ConverterContext(modelService, objectMapper);
                List<AbstractModel> decisionTables = modelService.getModelsByModelType(AbstractModel.MODEL_TYPE_DECISION_TABLE);
                decisionTables.forEach(abstractModel -> {
                    Model model = (Model) abstractModel;
                    converterContext.addDecisionTableModel(model);
                });
                ObjectNode modelNode = bpmnJsonConverter.convertToJson(bpmnModel, converterContext);
                this.setProcessPropertiesToKey(modelNode, processModel.getKey());
                AbstractModel savedModel = modelService.saveModel(modelId, processModel.getName(), processModel.getKey(),
                        processModel.getDescription(), modelNode.toString(), false,
                        null, user.getUsername());
                LambdaQueryWrapper<ModelInfo> modelInfoLambdaQueryWrapper = new LambdaQueryWrapper<>();
                modelInfoLambdaQueryWrapper.eq(ModelInfo::getModelId, savedModel.getId());
                ModelInfo modelInfo = modelInfoService.getOne(modelInfoLambdaQueryWrapper);
                modelInfo.setStatus(ModelFormStatusEnum.DFB.getStatus());
                modelInfo.setExtendStatus(ModelFormStatusEnum.DFB.getStatus());
                modelInfoService.saveOrUpdate(modelInfo);
                return R.ok(savedModel.getId());
            } catch (Exception e) {
                return  R.failed( "bpmn.js failed for " + fileName + ", error message " + e.getMessage());
            }
        } else {
            return R.failed("Invalid file name, only .bpmn and .bpmn20.xml files are supported not " + fileName);
        }
    }

    private void setProcessPropertiesToKey(ObjectNode modelNode, String key) {
        ObjectNode objectNode = (ObjectNode) modelNode.get("properties");
        objectNode.put("process_id", key);
        objectNode.put("processid", key);
    }

    @Override
    public R<String> publishBpmn(String modelId) {
        R<String> returnVo;
        Model model = modelService.getModel(modelId);
        BpmnModel bpmnModel = modelService.getBpmnModel(model);
        returnVo = this.validationErrors(bpmnModel);
        if (returnVo.getCode() != 0) {
            return returnVo;
        }
        LambdaQueryWrapper<ModelInfo> modelInfoLambdaQueryWrapper = new LambdaQueryWrapper<>();
        modelInfoLambdaQueryWrapper.eq(ModelInfo::getModelId, modelId);
        ModelInfo modelInfo = modelInfoService.getOne(modelInfoLambdaQueryWrapper);
        if (modelInfo != null) {
            R<String> statusReturnVo = ModelFormStatusEnum.checkActive(modelInfo.getStatus(), modelInfo.getExtendStatus());
            if (statusReturnVo.getCode()== CommonConstants.SUCCESS) {
                this.deployBpmn(modelInfo);
                LambdaUpdateWrapper<ModelInfo> modelInfoLambdaUpdateWrapper = new LambdaUpdateWrapper<>();
                modelInfoLambdaUpdateWrapper.set(ModelInfo::getStatus, ModelFormStatusEnum.YFB.getStatus())
                        .set(ModelInfo::getExtendStatus, ModelFormStatusEnum.YFB.getStatus())
                        .eq(ModelInfo::getModelId, modelId);
                modelInfoService.update(modelInfoLambdaUpdateWrapper);
            } else {
                return R.failed(statusReturnVo.getMsg());
            }
        } else {
            returnVo = R.failed( "没有找到对应的模型，请确认!");
        }
        return returnVo;
    }

    @Override
    public R<Model> createInitBpmn(ModelInfo modelInfo, CloudUser user) {
        ModelRepresentation modelRepresentation = new ModelRepresentation();
        modelRepresentation.setModelType(AbstractModel.MODEL_TYPE_BPMN);
        modelRepresentation.setKey(modelInfo.getModelKey());
        modelRepresentation.setName(modelInfo.getName());
        modelRepresentation.setTenantId(modelInfo.getAppSn());
        modelRepresentation.setLastUpdated(new Date());
        return flowableModelService.createModel(modelRepresentation, user);
    }

    @Override
    public R<Deployment> deployBpmn(ModelInfo modelInfo) {
        Model model = modelService.getModel(modelInfo.getModelId());
        BpmnModel bpmnModel = modelService.getBpmnModel(model);
        Deployment deploy = repositoryService.createDeployment()
                .name(model.getName())
                .key(model.getKey())
                .category(modelInfo.getCategoryCode())
                .tenantId(SecurityUtils.getUser().getTenantId().toString())
                .addBpmnModel(model.getKey() + BPMN_EXTENSION, bpmnModel)
                .deploy();

        return R.ok(deploy);
    }

    @Override
    public ModelInfoVo loadBpmnXmlByModelId(String modelId) {
        Model model = modelService.getModel(modelId);
        byte[] bpmnXML = modelService.getBpmnXML(model);
        String streamStr = new String(bpmnXML);
        ModelInfoVo modelInfoVo = new ModelInfoVo();
        modelInfoVo.setModelId(modelId);
        modelInfoVo.setModelName(model.getName());
        modelInfoVo.setModelKey(model.getKey());
        modelInfoVo.setFileName(model.getName());
        modelInfoVo.setModelXml(streamStr);
        return modelInfoVo;
    }

    @Override
    public ModelInfoVo loadBpmnXmlByModelKey(String modelKey) {
        if (StringUtils.isNotBlank(modelKey)) {
            ModelInfo info = modelInfoService.getModelInfoByModelKey(modelKey);
            if (info != null) {
                return this.loadBpmnXmlByModelId(info.getModelId());
            }
        }
        return null;
    }

}
